package ru.ifmo.cppsc.lexer;

import org.apache.log4j.Logger;
import ru.ifmo.cppsc.lexer.token.Token;
import ru.ifmo.cppsc.lexer.token.TokenType;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

/**
 * Lexycal analyser
 *
 * @author Oleg Rybak
 */
public class Lexer {

    private static final Logger log = Logger.getLogger(Lexer.class.getName());

    private enum LEXER_STATE {
        START,
        ERROR,
        INTEGER_LITERAL,
        DOUBLE_LITERAL,
        STRING_LITERAL,
        SINGLE_LEXEM,
        MULTI_LEXEM_START,
        MULTI_LEXEM,
        COMMENT,
        KEYWORD_START,
        KEYWORD,
        IDENTIFIER
    }

    private boolean isVarDef = false;
    private boolean isStruct = false;
    private Token typeHolderToken;
    private boolean isGoto = false;
    private boolean isNew = false;

    /**
     * Keywords
     */
    private Map<String, TokenType> keywords = new HashMap<String, TokenType>();

    /**
     * Lexems of one symbol
     */
    private Map<String, TokenType> singleLexems = new HashMap<String, TokenType>();

    /**
     * Lexems of one or two symbols
     */
    private Map<String, TokenType> multiLexems = new HashMap<String, TokenType>();


    private ArrayList<String> udefTypes = new ArrayList<String>();
    private StringBuffer buffer = new StringBuffer();
    private LEXER_STATE state = LEXER_STATE.START;
    private ArrayList<Token> tokens = new ArrayList<Token>();
    IdTable idTable = new IdTable();
    private int line = 1;

    public Lexer() {
        state = LEXER_STATE.START;
        isStruct = false;
        isGoto = false;
        isVarDef = false;
        isNew = false;

        keywords.put("goto", TokenType.TGoto);
        keywords.put("if", TokenType.TIf);
        keywords.put("then", TokenType.TThen);
        keywords.put("else", TokenType.TElse);
        keywords.put("do", TokenType.TDo);
        keywords.put("while", TokenType.TWhile);
        keywords.put("true", TokenType.TTrue);
        keywords.put("false", TokenType.TFalse);
        keywords.put("operator", TokenType.TOperator);
        keywords.put("new", TokenType.TNew);
        keywords.put("return", TokenType.TReturn);
        /// types
        keywords.put("int", TokenType.TInt);
        keywords.put("double", TokenType.TDouble);
        keywords.put("bool", TokenType.TBool);
        keywords.put("struct", TokenType.TStruct);
        keywords.put("void", TokenType.TVoid);


        singleLexems.put("{", TokenType.TLeftCurly);
        singleLexems.put("}", TokenType.TRightCurly);
        singleLexems.put("(", TokenType.TLeftParen);
        singleLexems.put(")", TokenType.TRightParen);
        singleLexems.put("[", TokenType.TLeftBracket);
        singleLexems.put("]", TokenType.TRightBracket);
        singleLexems.put(";", TokenType.TSemiColon);
        singleLexems.put("*", TokenType.TMultiply);
        singleLexems.put(",", TokenType.TComma);
        singleLexems.put(":", TokenType.TColon);
        singleLexems.put(".", TokenType.TDot);
        singleLexems.put("%", TokenType.TModulo);
        singleLexems.put("+", TokenType.TPlus);

        multiLexems.put("/", TokenType.TDivide);
        //multiLexems.put("++", TokenType.)
        multiLexems.put("<", TokenType.TLessThen);
        multiLexems.put(">", TokenType.TGreaterThen);
        multiLexems.put("<=", TokenType.TLessEqual);
        multiLexems.put(">=", TokenType.TGreaterEqual);
        multiLexems.put("==", TokenType.TEqual);
        multiLexems.put("=", TokenType.TAssign);
        multiLexems.put("!=", TokenType.TNotEqual);
        multiLexems.put("->", TokenType.TArrow);
        multiLexems.put("-", TokenType.TMinus);
        multiLexems.put("&&", TokenType.TLogicalAnd);
        multiLexems.put("||", TokenType.TLogicalOr);
    }


    @SuppressWarnings("unchecked")
    public ArrayList<Token> parse(String text) {
        line = 0;
        state = LEXER_STATE.START;
        tokens = new ArrayList<Token>();
        isStruct = false;

        for (int i = 0; i < text.length(); i++) {
            processSymbol(text.charAt(i));
        }
        processSymbol('$');

        return (ArrayList<Token>) tokens.clone();
    }


    private void processSymbol(char c) {
        if (c == '\n') {
            line++;
        }
        switch (state) {
            case START:
                doStart(c);
                break;
            case MULTI_LEXEM_START:
                doMultiLexem(c);
                break;
            case COMMENT:
                doComment(c);
                break;
            case KEYWORD_START:
                doKeywordStart(c);
                break;
            case KEYWORD:
                doKeyword(c);
                break;
            case IDENTIFIER:
                doIdentifier(c);
                break;
            case STRING_LITERAL:
                doStringLiteral(c);
                break;
            case INTEGER_LITERAL:
                doIntegerLiteral(c);
                break;
            case DOUBLE_LITERAL:
                doDoubleLiteral(c);
                break;
        }
    }


    /**
     * Start state worker
     *
     * @param c current character under a caret
     */
    private void doStart(char c) {
        log.info("PROCESSING STATE: [START]");
        assert (buffer.length() == 0);
        String s = Character.toString(c);
        // here parsing single symbol lexems
        if (singleLexems.containsKey(s)) {
            log.info("Adding single lexem: [" + s + "]");
            tokens.add(new Token(singleLexems.get(s), -1, "" + c, line, s, null, null));
            return;
        }
        /*
         * found symbol that is the beginning of multi symbol lexem
         * or one symbol lexem 
         */
        if (multiLexems.containsKey(s)) {
            buffer.append(s);
            state = LEXER_STATE.MULTI_LEXEM_START;
            return;
        }
        // beginning of integer literal
        if (isDigit(c)) {
            buffer.append(s);
            state = LEXER_STATE.INTEGER_LITERAL;
            return;
        }
        // beginning of string literal

        if (c == '\"') {
            buffer.append(s);
            state = LEXER_STATE.STRING_LITERAL;
            return;
        }

        if (isKeywordPrefix(s)) {
            buffer.append(s);
            state = LEXER_STATE.KEYWORD_START;
            return;
        }

        if (isAlpha(c)) {
            buffer.append(s);
            state = LEXER_STATE.IDENTIFIER;
            //return;
        }
        //return;
    }

    private void doMultiLexem(char c) {
        log.info("PROCESSING STATE: [MULTI_LEXEM_START]");
        assert (buffer.length() == 1);
        String s = buffer.toString();
        String str = s + c;
        if (str.equals("//")) {
            state = LEXER_STATE.COMMENT;
            return;
        }

        if (multiLexems.containsKey(s + c)) {
            s = s + c;
            tokens.add(new Token(multiLexems.get(s), -1, s, line, s, null, null));
            restart();
            return;
        }

        tokens.add(new Token(multiLexems.get(s), -1, s, line, s, null, null));
        restart(c);
    }


    private void doComment(char ch) {
        log.info("PROCESSING STATE: [COMMENT]");
        if (ch == '\n') {
            log.debug("Comment ended");
            state = LEXER_STATE.START;
            buffer.delete(0, buffer.length());
            return;
        }
        buffer.append(ch);
    }

    private void doKeywordStart(char ch) {
        log.info("PROCESSING STATE: [KEYWORD_START]");
        String s = buffer.toString();
        if (isAlpha(ch)) {
            s = s + ch;
            if (isKeyword(s)) {
                buffer.append(ch);
                state = LEXER_STATE.KEYWORD;
                return;
            }

            if (isKeywordPrefix(s)) {
                buffer.append(ch);
                return;

            }
        }

        state = LEXER_STATE.IDENTIFIER;
        doIdentifier(ch);
    }

    private void doKeyword(char ch) {
        log.info("PROCESSING STATE: [KEYWORD]");
        String s = buffer.toString();
        assert (isKeyword(s));

        if (isAlpha(ch) || isDigit(ch)) {
            buffer.append(ch);

            if (isKeywordPrefix(s)) {
                state = LEXER_STATE.KEYWORD_START;    // do 
            } else {
                state = LEXER_STATE.IDENTIFIER;
            }
            return;
        }

        System.out.println("-------------------------keyword: " + s);
        if (keywords.get(s) == TokenType.TStruct && !isStruct) {
            isStruct = true;
        } else if (keywords.get(s) == TokenType.TGoto) {
            isGoto = true;
        } else if (keywords.get(s) == TokenType.TNew) {
            isNew = true;
            tokens.add(new Token(keywords.get(s), -1, buffer.toString(), line, s, null, null));
        } else {
            TokenType t = keywords.get(s);
            if (t == TokenType.TUserType || t == TokenType.TBool ||
                    t == TokenType.TVoid || t == TokenType.TInt || t == TokenType.TDouble) {
                if (isNew) {
                    tokens.add(new Token(keywords.get(s), -1, buffer.toString(), line, s, null, null));
                } else {
                    typeHolderToken = new Token(keywords.get(s), -1, buffer.toString(), line, s, null, null);
                    isVarDef = true;
                }
            } else {
                tokens.add(new Token(keywords.get(s), -1, buffer.toString(), line, s, null, null));
                isVarDef = false;
            }
            isStruct = false;
        }

        restart(ch);
    }

    private void doIdentifier(char ch) {
        log.info("PROCESSING STATE: [IDENTIFIER]");
        if (isAlpha(ch) || isDigit(ch)) {
            buffer.append(ch);
            return;
        }

        if (isStruct) {
            isStruct = false;
            updateIdTable(TokenType.TUserType);
        } else {
            updateIdTable(TokenType.TIdentifier);
        }

        state = LEXER_STATE.START;
        doStart(ch);
    }

    private void doStringLiteral(char ch) {
        if (ch == '\n') {
            state = LEXER_STATE.ERROR;
            return;
        }

        buffer.append(ch);

        if (ch == '\"') {
            updateIdTable(TokenType.TStringLiteral);
            state = LEXER_STATE.START;
            //return;
        }
    }

    private void doIntegerLiteral(char ch) {
        if (isDigit(ch)) {
            buffer.append(ch);
            return;
        }

        if (ch == '.') {
            buffer.append(ch);
            state = LEXER_STATE.DOUBLE_LITERAL;
            return;
        }

        if (isSeparator(ch)) {
            updateIdTable(TokenType.TIntegerLiteral);
            state = LEXER_STATE.START;
            return;
        }

        String s = Character.toString(ch);
        if (singleLexems.containsKey(s) || multiLexems.containsKey(s)) {
            updateIdTable(TokenType.TIntegerLiteral);
            state = LEXER_STATE.START;
            processSymbol(ch);
            return;
        }

        state = LEXER_STATE.ERROR;
    }

    private void doDoubleLiteral(char ch) {
        if (isDigit(ch)) {
            buffer.append(ch);
            return;
        }

        updateIdTable(TokenType.TDoubleLiteral);
        restart(ch);
    }

    /// -----------------------

    private boolean isKeyword(String s) {
        boolean result = false;
        for (String str : keywords.keySet()) {
            if (str.equals(s)) {
                result = true;
            }
        }
        return result;
    }

    private boolean isKeywordPrefix(String s) {
        boolean result = false;
        for (String str : keywords.keySet()) {
            if (str.startsWith(s)) {
                result = true;
                break;
            }
        }
        return result;
    }

    private void updateIdTable(TokenType type) {
        String s = buffer.toString();
        log.debug("updating idTable: [" + type.getName() + "][" + s + "]");
        int idx = idTable.search(s);
        if (idx == -1) {
            idx = idTable.put(new Id(s));
        }
        //tokens.add(new Token(type, idx, "", line));

        if (isVarDef) {
            isVarDef = false;
            tokens.add(new Token(TokenType.TVarDef, -1, "vardef", line, "vardef",
                    typeHolderToken, new Token(type, idx, s, line, "id", null, null)
            ));
        } else if (isGoto) {
            isGoto = false;
            tokens.add(new Token(TokenType.TGoto, -1, "goto", line, "goto",
                    null, new Token(type, idx, s, line, "id", null, null)
            ));
        } else {
            if (type != TokenType.TUserType) {
                if (udefTypes.contains(s)) {
                    //tokens.add(new Token(type, idx, s, line, "utype", null, null));
                    typeHolderToken = new Token(type, idx, s, line, "utype", null, null);
                    isVarDef = true;
                } else {
                    if (type == TokenType.TIntegerLiteral) {
                        tokens.add(new Token(type, idx, s, line, "IntegerLiteral", null, null));
                    } else if (type == TokenType.TDoubleLiteral) {
                        tokens.add(new Token(type, idx, s, line, "DoubleLiteral", null, null));
                    } else if (type == TokenType.TStringLiteral) {
                        tokens.add(new Token(type, idx, s, line, "StringLiteral", null, null));
                    } else {
                        tokens.add(new Token(type, idx, s, line, "id", null, null));
                    }
                }
            } else {
                tokens.add(new Token(type, idx, s, line, "utype", null, null));
                udefTypes.add(s);
            }

        }


        buffer.delete(0, buffer.length());
    }

    void restart() {
        buffer.delete(0, buffer.length());
        state = LEXER_STATE.START;
    }

    void restart(char c) {
        restart();
        processSymbol(c);
    }

    private static boolean isDigit(char ch) {
        return '0' <= ch && ch <= '9';
    }

    private static boolean isAlpha(char c) {
        return 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z';
    }

    private static boolean isSeparator(char c) {
        return (c == ' ' || c == '\n' || c == '\r' || c == '\t');
    }
}

